//------------------------------------------------------------------------------
// <copyright file="EFOracleProviderServices.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//------------------------------------------------------------------------------

namespace EFOracleOdpProvider
{
    using System;
    using System.Collections.Generic;
    using System.Data.Common;
    using System.Data.Common.CommandTrees;
    using System.Data.Common.Utils;
    using System.Data.Metadata.Edm;
    using System.Diagnostics;
    using System.Data;
    using Oracle.DataAccess;
    using Oracle.DataAccess.Client;

    /// <summary>
    /// The ProviderServices object for the namespace EFOracleOdpProvider
    /// </summary>
    internal sealed class EFOracleOdpProviderServices : DbProviderServices {

        /// <summary>
        /// Singleton object;
        /// </summary>
        internal static readonly EFOracleOdpProviderServices Instance = new EFOracleOdpProviderServices();

        /// <summary>
        /// Create a Command Definition object, given the connection and command tree
        /// </summary>
        /// <param name="providerManifest">provider manifest that was determined from metadata</param>
        /// <param name="commandTree">command tree for the statement</param>
        /// <returns>an exectable command definition object</returns>
        protected override DbCommandDefinition CreateDbCommandDefinition(DbProviderManifest providerManifest, DbCommandTree commandTree) {
            Debug.Assert(providerManifest != null, "CreateCommandDefinition passed null provider manifest to CreateDbCommandDefinition?");
            Debug.Assert(commandTree != null, "CreateCommandDefinition did not validate commandTree argument?");

            if (!(providerManifest is EFOracleOdpProviderManifest)) {
                throw new ArgumentException(string.Format("The provider manifest given is not of type '{0}'.",
                                typeof(EFOracleOdpProviderServices)));
            }

            DbCommand prototype = CreateCommand((EFOracleOdpProviderManifest)providerManifest, commandTree);
            DbCommandDefinition result = this.CreateCommandDefinition(prototype);
            return result;
        }

        /// <summary>
        /// Create a EFOracleCommand object, given the provider manifest and command tree
        /// </summary>
        /// <param name="providerManifest">provider manifest</param>
        /// <param name="commandTree">command tree for the statement</param>
        /// <returns>a command object</returns>
        private DbCommand CreateCommand(EFOracleOdpProviderManifest providerManifest, DbCommandTree commandTree) {
            EntityUtils.CheckArgumentNull(providerManifest, "providerManifest");
            EntityUtils.CheckArgumentNull(commandTree, "commandTree");

            EFOracleVersion serverVersion = EFOracleVersionUtils.GetStorageVersion(providerManifest.Token);
            EFOracleOdpCommand command = new EFOracleOdpCommand();

            if(Debugger.IsLogging())
            {
                Debug.WriteLine(string.Format("EFOracleOdpProviderServices.CreateCommandDefinition: serverVersion={0} commandTree={1}\n", (int)serverVersion, commandTree));
            }

            // Set expected column types for DbQueryCommandTree
            DbQueryCommandTree queryTree = commandTree as DbQueryCommandTree;
            if (queryTree != null)
            {
                DbProjectExpression projectExpression = queryTree.Query as DbProjectExpression;
                if (projectExpression != null)
                {
                    EdmType resultsType = projectExpression.Projection.ResultType.EdmType;

                    StructuralType resultsAsStructuralType = resultsType as StructuralType;
                    if (resultsAsStructuralType != null)
                    {
                        command.ExpectedColumnTypes = new PrimitiveType[resultsAsStructuralType.Members.Count];

                        for (int ordinal = 0; ordinal < resultsAsStructuralType.Members.Count; ordinal++)
                        {
                            EdmMember member = resultsAsStructuralType.Members[ordinal];
                            PrimitiveType primitiveType = member.TypeUsage.EdmType as PrimitiveType;
                            command.ExpectedColumnTypes[ordinal] = primitiveType;
                        }
                    }
                }
            }

            // Set expected column types for DbFunctionCommandTree
            DbFunctionCommandTree functionTree = commandTree as DbFunctionCommandTree;
            if (functionTree != null)
            {
                if (functionTree.ResultType != null)
                {
                    Debug.Assert(MetadataHelpers.IsCollectionType(functionTree.ResultType.EdmType),
                        "Result type of a function is expected to be a collection of RowType or PrimitiveType");

                    EdmType elementType = MetadataHelpers.GetElementTypeUsage(functionTree.ResultType).EdmType;

                    if (MetadataHelpers.IsRowType(elementType))
                    {
                        ReadOnlyMetadataCollection<EdmMember> members = ((RowType)elementType).Members;
                        command.ExpectedColumnTypes = new PrimitiveType[members.Count];

                        for (int ordinal = 0; ordinal < members.Count; ordinal++)
                        {
                            EdmMember member = members[ordinal];
                            PrimitiveType primitiveType = (PrimitiveType)member.TypeUsage.EdmType;
                            command.ExpectedColumnTypes[ordinal] = primitiveType;
                        }

                    }
                    else if (MetadataHelpers.IsPrimitiveType(elementType))
                    {
                        command.ExpectedColumnTypes = new PrimitiveType[1];
                        command.ExpectedColumnTypes[0] = (PrimitiveType)elementType;
                    }
                    else
                    {
                        Debug.Fail("Result type of a function is expected to be a collection of RowType or PrimitiveType");
                    }
                }
            }


            List<OracleParameter> parameters;
            CommandType commandType = CommandType.Text;
            command.CommandText = EFOracleOdpProvider.SqlGen.SqlGenerator.GenerateSql(commandTree, providerManifest, serverVersion, out parameters, out commandType);
            command.CommandType = commandType;

            if (Debugger.IsLogging())
            {
                Debug.WriteLine(string.Format("EFOracleOdpProviderServices.CreateCommandDefinition: Generated SQL={0}\n", command.CommandText));
            }

            // Get the function (if any) implemented by the command tree since this influences our interpretation of parameters
            EdmFunction function = null;
            if (commandTree is DbFunctionCommandTree) {
                function = ((DbFunctionCommandTree)commandTree).EdmFunction;
            }
            
            // Now make sure we populate the command's parameters from the CQT's parameters:
            foreach (KeyValuePair<string, TypeUsage> queryParameter in commandTree.Parameters) {
                OracleParameter parameter;

                // Use the corresponding function parameter TypeUsage where available (currently, the SSDL facets and 
                // type trump user-defined facets and type in the EntityCommand).
                FunctionParameter functionParameter;
                if (null != function && function.Parameters.TryGetValue(queryParameter.Key, false, out functionParameter)) {
                    parameter = CreateOracleParameter(functionParameter.Name, functionParameter.TypeUsage, functionParameter.Mode, DBNull.Value, serverVersion);
                }
                else {
                    parameter = CreateOracleParameter(queryParameter.Key, queryParameter.Value, ParameterMode.In, DBNull.Value, serverVersion);
                }
                command.Parameters.Add(parameter);
            }

            // Now add parameters added as part of SQL gen (note: this feature is only safe for DML SQL gen which
            // does not support user parameters, where there is no risk of name collision)
            if (null != parameters && 0 < parameters.Count) {
                if (commandTree is DbFunctionCommandTree &&
                    commandTree is DbDeleteCommandTree &&
                    commandTree is DbInsertCommandTree &&
                    commandTree is DbUpdateCommandTree) {
                        throw new InvalidOperationException("Internal error: SqlGenParametersNotPermitted");
                }

                foreach (OracleParameter parameter in parameters) {
                    command.Parameters.Add(parameter);
                }
            }

            return command;
        }

        protected override string GetDbProviderManifestToken(DbConnection connection) {
            EntityUtils.CheckArgumentNull(connection, "connection");

            EFOracleOdpConnection oracleConnection = connection as EFOracleOdpConnection;
            if (oracleConnection == null) {
                throw new ArgumentException(string.Format("The connection is not of type '{0}'.", typeof(EFOracleOdpConnection)));
            }

            if (string.IsNullOrEmpty(oracleConnection.ConnectionString)) {
                throw new ArgumentException("Could not determine storage version; " +
                            "a valid storage connection or a version hint is required.");
            }

            bool closeConnection = false;
            try {
                if (oracleConnection.State != ConnectionState.Open) {
                    oracleConnection.Open();
                    closeConnection = true;
                }
                EFOracleVersion version = EFOracleVersionUtils.GetStorageVersion(oracleConnection);
                return EFOracleVersionUtils.GetVersionHint(version);
            }
            finally {
                if (closeConnection) {
                    oracleConnection.Close();
                }
            }
        }

        protected override DbProviderManifest GetDbProviderManifest(string versionHint) {
            if (string.IsNullOrEmpty(versionHint)) {
                throw new ArgumentException("Could not determine storage version; " +
                            "a valid storage connection or a version hint is required.");
            }

            return new EFOracleOdpProviderManifest(versionHint);
        }

        /// <summary>
        /// Creates a OracleParameter given a name, type, and direction
        /// </summary>
        internal static OracleParameter CreateOracleParameter(string name, TypeUsage type, ParameterMode mode, object value, EFOracleVersion version) {
            int? size;
            byte? precision;
            byte? scale;
            
            OracleParameter result = new OracleParameter(name, value);

            // .Direction
            ParameterDirection direction = MetadataHelpers.ParameterModeToParameterDirection(mode);
            if (result.Direction != direction) {
                result.Direction = direction;
            }
            
            // .Size, .Precision, .Scale and .DbType
            // output parameters are handled differently (we need to ensure there is space for return
            // values where the user has not given a specific Size/MaxLength)
            bool isOutParam = mode != ParameterMode.In;
            OracleDbType dbType = GetDbType(type, isOutParam, version, out size, out precision, out scale);

            if (result.OracleDbType != dbType) {
                result.OracleDbType = dbType;
            }

            // Note that we overwrite 'facet' parameters where either the value is different or
            // there is an output parameter. This is because output parameters in SqlClient have their
            // facets clobbered if they are implicitly set (e.g. if the Precision was implicitly set
            // by setting the value)
            if (size.HasValue && (isOutParam || result.Size != size.Value)) {
                result.Size = size.Value;
            }
            if (precision.HasValue && (isOutParam || result.Precision != precision.Value)) {
                result.Precision = precision.Value;
            }
            if (scale.HasValue && (isOutParam || result.Scale != scale.Value)) {
                result.Scale = scale.Value;
            }

            // .IsNullable
            bool isNullable = MetadataHelpers.IsNullable(type);
            if (isOutParam || isNullable != result.IsNullable) {
                result.IsNullable = isNullable;
            }

            return result;
        }


        /// <summary>
        /// Determines database type for the given primitive type. Extracts facet
        /// information as well.
        /// </summary>
        private static OracleDbType GetDbType(TypeUsage type, bool isOutParam, EFOracleVersion version, out int? size, out byte? precision, out byte? scale) {
            // only supported for primitive type
            PrimitiveTypeKind primitiveTypeKind = MetadataHelpers.GetPrimitiveTypeKind(type);

            size = default(int?);
            precision = default(byte?);
            scale = default(byte?);

            // CONSIDER add logic for Xml here
            switch (primitiveTypeKind) {
                case PrimitiveTypeKind.Binary:
                    // for output parameters, ensure there is space...
                    size = GetParameterSize(type, isOutParam);
                    return GetBinaryDbType(type);

                case PrimitiveTypeKind.Boolean:
                    // ##ORACLE TODO
                    return OracleDbType.Byte;

                case PrimitiveTypeKind.Byte:
                    return OracleDbType.Byte;

                case PrimitiveTypeKind.Time:
                    // ##ORACLE TODO
                    return OracleDbType.TimeStamp;

                case PrimitiveTypeKind.DateTimeOffset:
                    // ##ORACLE TODO
                    return OracleDbType.TimeStamp;

                case PrimitiveTypeKind.DateTime:
                    // ##ORACLE TODO choose appropriate time based on information in metadata
                    //return OracleDbType.DateTime;
                    return OracleDbType.Date;

                case PrimitiveTypeKind.Decimal:
                    precision = GetParameterPrecision(type, null);
                    scale = GetScale(type);
                    //return OracleDbType.Number;
                    return OracleDbType.Decimal;

                case PrimitiveTypeKind.Double:
                    return OracleDbType.Double;

                case PrimitiveTypeKind.Guid:
                    return OracleDbType.Raw;

                case PrimitiveTypeKind.Int16:
                    return OracleDbType.Int16;

                case PrimitiveTypeKind.Int32:
                    return OracleDbType.Int32;

                case PrimitiveTypeKind.Int64:
                    precision = 20;
                    scale = 0;
                    //return OracleDbType.Number;
                    return OracleDbType.Int64;

                case PrimitiveTypeKind.SByte:
                    return OracleDbType.Byte;

                case PrimitiveTypeKind.Single:
                    //return OracleDbType.Float;
                    return OracleDbType.Single;

                case PrimitiveTypeKind.String:
                    size = GetParameterSize(type, isOutParam);
                    return GetStringDbType(type);

                default:
                    throw new InvalidOperationException("unknown PrimitiveTypeKind " + primitiveTypeKind);
            }
        }

        /// <summary>
        /// Determines preferred value for OracleParameter.Size. Returns null
        /// where there is no preference.
        /// </summary>
        private static int? GetParameterSize(TypeUsage type, bool isOutParam) {
            int maxLength;
            if (MetadataHelpers.TryGetMaxLength(type, out maxLength)) {
                // if the MaxLength facet has a specific value use it
                return maxLength;
            }
            else if (isOutParam) {
                // if the parameter is a return/out/inout parameter, ensure there 
                // is space for any value
                // return int.MaxValue;

                // ##ORACLE
                // TODO need to double check appropriate value for Oracle
                return default(int?);
            }
            else {
                // no value
                return default(int?);
            }
        }

        /// <summary>
        /// Returns OracleParameter.Precision where the type facet exists. Otherwise,
        /// returns null.
        /// </summary>
        private static byte? GetParameterPrecision(TypeUsage type, byte? defaultIfUndefined) {
            byte precision;
            if (MetadataHelpers.TryGetPrecision(type, out precision)) {
                return precision;
            }
            else {
                return defaultIfUndefined;
            }
        }

        /// <summary>
        /// Returns OracleParameter.Scale where the type facet exists. Otherwise,
        /// returns null.
        /// </summary>
        private static byte? GetScale(TypeUsage type) {
            byte scale;
            if (MetadataHelpers.TryGetScale(type, out scale)) {
                return scale;
            }
            else {
                return default(byte?);
            }
        }

        /// <summary>
        /// Chooses the appropriate database type for the given string type.
        /// </summary>
        private static OracleDbType GetStringDbType(TypeUsage type) {
            Debug.Assert(type.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType &&
                PrimitiveTypeKind.String == ((PrimitiveType)type.EdmType).PrimitiveTypeKind, "only valid for string type");

            OracleDbType dbType;
            if (type.EdmType.Name.ToLowerInvariant() == "xml") {
                //dbType = OracleDbType.NVarChar;
                dbType = OracleDbType.NVarchar2;
            }
            else {
                // Specific type depends on whether the string is a unicode string and whether it is a fixed length string.
                // By default, assume widest type (unicode) and most common type (variable length)
                bool unicode;
                bool fixedLength;
                if (!MetadataHelpers.TryGetIsFixedLength(type, out fixedLength)) {
                    fixedLength = false;
                }

                if (!MetadataHelpers.TryGetIsUnicode(type, out unicode)) {
                    unicode = true;
                }

                if (fixedLength) {
                    dbType = (unicode ? OracleDbType.NChar : OracleDbType.Char);
                }
                else {
                    //dbType = (unicode ? OracleDbType.NVarChar : OracleDbType.VarChar);
                    dbType = (unicode ? OracleDbType.NVarchar2 : OracleDbType.Varchar2);
                }
            }
            return dbType;
        }

        /// <summary>
        /// Chooses the appropriate database type for the given binary type.
        /// </summary>
        private static OracleDbType GetBinaryDbType(TypeUsage type) {
            Debug.Assert(type.EdmType.BuiltInTypeKind == BuiltInTypeKind.PrimitiveType &&
                PrimitiveTypeKind.Binary == ((PrimitiveType)type.EdmType).PrimitiveTypeKind, "only valid for binary type");

            // Specific type depends on whether the binary value is fixed length. By default, assume variable length.
            bool fixedLength;
            if (!MetadataHelpers.TryGetIsFixedLength(type, out fixedLength)) {
                fixedLength = false;
            }

            // ##ORACLE
            // TODO choose appropriate type based on information in facets
            return fixedLength ? OracleDbType.Blob : OracleDbType.Blob;
        }
    }
}